package com.inikolova.numericpuzzle;

import java.util.Random;

/**
 * Instances of this class represent a numeric puzzle. It is a two-dimensional grid that is 
 * initialized with random numbers. The class provides methods for moving the cells of the puzzle
 * until a target state is reached.
 * @author nikol
 *
 */
public class NumericPuzzle {
	public static final int FREE_CELL_ID = 0;
	
	private int[][] currentState = null;
	private int[][] targetState = null;
	private int height = 0;
	private int width = 0;
	private int cellsCount = 0;
	
	private Position freeCellPosition = null;
	
	/**
	 * Default constructor
	 * @param targetState - An array representing the target state the numeric puzzle should reach in order to be ordered
	 */
	public NumericPuzzle(int[][] targetState) {
		this.targetState = targetState;
		
		// Init the puzzle size
		this.width = targetState[0].length;
		this.height = targetState.length;
		
		// Fill the current state with random numbers in the range [0..maxId)
		Random randomGenerator = new Random();
		this.cellsCount = this.height * this.width;
		
		currentState = new int[this.height][this.width];
		
		// Initially this list contains only 0-es
		boolean[] visitedCellIdList = new boolean[this.cellsCount];
		for (int i = 0; i < visitedCellIdList.length; i++) {
			visitedCellIdList[i] = false;
		}

		for (int x = 0; x < this.width; x++) {
			int y = 0;
			while (y < this.height) {
				int cellId = randomGenerator.nextInt(this.cellsCount);
				if (visitedCellIdList[cellId] == false) {
					currentState[x][y] = cellId;
					if (currentState[x][y] == FREE_CELL_ID) {
						this.freeCellPosition = new Position(x, y);
					}
					visitedCellIdList[cellId] = true;
					y++;
				}
			}
		}
	}
	
	/**
	 * Gets the height of the puzzle in number of cells
	 * @return The height of the puzzle
	 */
	public int getHeight() {
		return this.height;
	}
	
	/**
	 * Gets the width of the puzzle in number of cells
	 * @return The width of the puzzle
	 */
	public int getWidth() {
		return this.width;
	}
	
	/**
	 * Gets the total number of cells in the puzzle
	 * @return
	 */
	public int getCellsCount() {
		return this.cellsCount;
	}
	

	/**
	 * Gets the ID of a cell
	 * @param position - the position of the cell in the puzzle
	 * @return The ID of the cell at the specified position
	 */
	public int getCellId(Position position) {
		return this.currentState[position.getX()][position.getY()];
	}
	
	/**
	 * Gets the position of a cell
	 * @param cellId - the ID of the cell
	 * @return The position of the cell in the puzzle
	 */
	public Position getCellPostion(int cellId) {
		// Find the position of the cell in the current state
		for (int x = 0; x < this.width; x++) {
			for (int y = 0; y < this.height; y++) {
				if (this.currentState[x][y] == cellId) {
					return new Position(x,y);
				}
			}
		}
		throw new IllegalArgumentException("Cell with ID " + cellId + " does not exist");
	}
	
	/**
	 * Checks if the puzzle is ordered
	 * @return True if the puzzle is ordered, i.e. the target state is reached
	 */
	public boolean isOrdered() {
		for (int x = 0; x < this.width; x++) {
			for (int y = 0; y < this.height; y++) {
				if (this.currentState[x][y] != this.targetState[x][y]) {
					return false;
				}
			}
		}
		return true;
	}
	
	/**
	 * Checks if a cell can be moved to the free call's location
	 * @param cellPosition - position of a cell in the puzzle  
	 * @return True if the cell at the specified position can be moved to the free cell's position
	 */
	private boolean isSwapWithFreeCellAllowed(Position cellPosition) {
		// Only direct neighbor cells are allowed to be swapped with the free cell. Thus, swap is allowed if:
		// Cells are in the same row => Y coordinates match and X coordinates differ with 1
		// or if:
		// Cells are in the same column => X coordinates match and Y coordinates differ with 1
		boolean isAllowed =
			((cellPosition.getX() == this.freeCellPosition.getX())
				&& (Math.abs(cellPosition.getY() - this.freeCellPosition.getY())) == 1)
			|| ((cellPosition.getY() == this.freeCellPosition.getY())
				&& (Math.abs(cellPosition.getX() - this.freeCellPosition.getX()) == 1));

		//System.out.println("Is allowed to swap: " + isAllowed);
		return isAllowed;
	}
	
	/**
	 * Checks if a cell can be moved to the free call's location
	 * @param cellId - the ID of a cell
	 * @return True if the cell at the specified position can be moved to the free cell's position
	 */
	public boolean isSwapWithFreeCellAllowed(int cellId) {
		Position cellPosition = getCellPostion(cellId);
		if (cellPosition != null) {
			return isSwapWithFreeCellAllowed(cellPosition);
		}
		return false;
	}
	
	/**
	 * Puts a cell into the free cell's location
	 * @param cellPosition - the position of the cell that will be swapped with the free cell
	 */
	private void swapWithFreeCell(Position cellPosition) {
		int bufferId = this.currentState[cellPosition.getX()][cellPosition.getY()];
		Position bufferPosition = new Position(this.freeCellPosition);
		
		this.currentState[cellPosition.getX()][cellPosition.getY()] = FREE_CELL_ID;
		this.freeCellPosition = new Position(cellPosition);
		
		this.currentState[bufferPosition.getX()][bufferPosition.getY()] = bufferId;
	}
	
	/**
	 * Puts a cell into the free cell's location
	 * @param cellId - the ID of the cell that will be swapped with the free cell
	 */
	public void swapWithFreeCell(int cellId) {
		Position cellPosition = getCellPostion(cellId);
		if (cellPosition != null) {
			swapWithFreeCell(cellPosition);
		}
	}
	
	/**
	 * String representation of the puzzle
	 */
	public String toString() {
		StringBuffer output = new StringBuffer();

		output.append("Current state of the puzzle\t");
		for (int x = 0; x < this.width; x++) {
			for (int y = 0; y < this.height; y++) {
				output.append(currentState[x][y] + "\t");
			}
			output.append("\t");
		}
		
		return output.toString();
	}
}
